/*

  这是为AI-Thinker ESP32-CAM和ESP-EYE模块实现的简单MJPEG流媒体网络服务器。
  经测试可与VLC和Blynk视频控件配合使用，最多支持10个同时连接的流媒体客户端。
  同步流媒体通过FreeRTOS任务实现。

  灵感来源于并基于此Instructable教程：9美元的RTSP视频流服务器（使用ESP32-CAM开发板）
  （https://www.instructables.com/id/9-RTSP-Video-Streamer-Using-the-ESP32-CAM-Board/）

  开发板：AI-Thinker ESP32-CAM或ESP-EYE
  编译设置：
   ESP32开发模块
   CPU频率：240
   闪存频率：80
   闪存模式：QIO
   闪存大小：4Mb
   分区：最小SPIFFS
   PSRAM：已启用
*/

// ESP32有两个核心：应用核心和处理核心（运行ESP32 SDK堆栈的核心）
#define APP_CPU 1
#define PRO_CPU 0

#include "OV2640.h"
#include <WebServer.h>
#include <WiFi.h>
#include <WiFiClient.h>

#include <driver/rtc_io.h>
#include <esp_bt.h>
#include <esp_sleep.h>
#include <esp_wifi.h>

// 选择相机型号
// #define CAMERA_MODEL_WROVER_KIT
// #define CAMERA_MODEL_ESP_EYE
// #define CAMERA_MODEL_M5STACK_PSRAM
// #define CAMERA_MODEL_M5STACK_WIDE
#define CAMERA_MODEL_AI_THINKER

#include "camera_pins.h"

/*
  下一个是包含WiFi凭据的头文件。
  你需要执行以下操作：

  1. 在同一文件夹中创建一个名为"home_wifi_multi.h"的文件，或放在Arduino IDE的"libraries"文件夹的单独子文件夹下。（实际上是创建一个"伪"库 - 我称之为"MySettings"）。
  2. 在文件中放置以下文本：
  #define SSID1 "替换为你的WiFi名称"
  #define PWD1 "替换为你的WiFi密码"
  3. 保存。

  这样应该就可以工作了
*/
#include "home_wifi_multi.h"

OV2640 cam;

WebServer server(80);

// ===== rtos任务句柄 =========================
// 流媒体通过3个任务实现：
TaskHandle_t tMjpeg;  // 处理客户端与网络服务器的连接
TaskHandle_t tCam;    // 处理从相机获取图像帧并本地存储
TaskHandle_t tStream; // 实际将帧流式传输到所有连接的客户端

// frameSync信号量用于防止在替换下一帧时流式传输缓冲区
SemaphoreHandle_t frameSync = NULL;

// 队列存储当前已连接的要流式传输的客户端
QueueHandle_t streamingClients;

const int FPS = 16;

// 我们将每50毫秒（20赫兹）处理一次Web客户端请求
const int WSINTERVAL = 100;

// ======== 服务器连接处理任务 ==========================
void mjpegCB(void *pvParameters)
{
    TickType_t xLastWakeTime;
    const TickType_t xFrequency = pdMS_TO_TICKS(WSINTERVAL);

    // 创建帧同步信号量并初始化
    frameSync = xSemaphoreCreateBinary();
    xSemaphoreGive(frameSync);

    // 创建队列以跟踪所有已连接的客户端
    streamingClients = xQueueCreate(10, sizeof(WiFiClient *));

    //=== 设置部分  ==================

    //  创建从相机抓取帧的RTOS任务
    xTaskCreatePinnedToCore(
        camCB,    // 回调函数
        "cam",    // 名称
        4096,     // 堆栈大小
        NULL,     // 参数
        2,        // 优先级
        &tCam,    // RTOS任务句柄
        APP_CPU); // 核心

    //  创建将流推送到所有已连接客户端的任务
    xTaskCreatePinnedToCore(
        streamCB,
        "strmCB",
        4 * 1024,
        NULL, //(void*) handler,
        2,
        &tStream,
        APP_CPU);

    //  注册网络服务器处理例程
    server.on("/mjpeg/1", HTTP_GET, handleJPGSstream);
    server.on("/jpg", HTTP_GET, handleJPG);
    server.onNotFound(handleNotFound);

    //  启动网络服务器
    server.begin();

    //=== 循环()部分  ===================
    xLastWakeTime = xTaskGetTickCount();
    for (;;)
    {
        server.handleClient();

        //  在每次服务器客户端处理请求后，我们让其他任务运行然后暂停
        taskYIELD();
        vTaskDelayUntil(&xLastWakeTime, xFrequency);
    }
}

// 常用变量：
volatile size_t camSize; // 当前帧的大小，字节
volatile char *camBuf;   // 指向当前帧的指针

// ==== 从相机抓取帧的RTOS任务 =========================
void camCB(void *pvParameters)
{

    TickType_t xLastWakeTime;

    //  与当前所需帧率相关的运行间隔
    const TickType_t xFrequency = pdMS_TO_TICKS(1000 / FPS);

    // 用于切换活动帧的关键部分的互斥锁
    portMUX_TYPE xSemaphore = portMUX_INITIALIZER_UNLOCKED;

    //  指向2个帧的指针、它们各自的大小和当前帧的索引
    char *fbs[2] = {NULL, NULL};
    size_t fSize[2] = {0, 0};
    int ifb = 0;

    //=== 循环()部分  ===================
    xLastWakeTime = xTaskGetTickCount();

    for (;;)
    {

        //  从相机抓取一帧并查询其大小
        cam.run();
        size_t s = cam.getSize();

        //  如果帧大小大于我们之前分配的大小 - 请求当前帧空间的125%
        if (s > fSize[ifb])
        {
            fSize[ifb] = s * 4 / 3;
            fbs[ifb] = allocateMemory(fbs[ifb], fSize[ifb]);
        }

        //  将当前帧复制到本地缓冲区
        char *b = (char *)cam.getfb();
        memcpy(fbs[ifb], b, s);

        //  让其他任务运行并等待当前帧率间隔结束（如果有剩余时间）
        taskYIELD();
        vTaskDelayUntil(&xLastWakeTime, xFrequency);

        //  仅当当前没有帧被流式传输到客户端时才切换帧
        //  等待信号量直到客户端操作完成
        xSemaphoreTake(frameSync, portMAX_DELAY);

        //  切换当前帧时不允许中断
        portENTER_CRITICAL(&xSemaphore);
        camBuf = fbs[ifb];
        camSize = s;
        ifb++;
        ifb &= 1; // 这应该产生1, 0, 1, 0, 1 ... 的序列
        portEXIT_CRITICAL(&xSemaphore);

        //  让等待帧的任何任务知道帧已准备好
        xSemaphoreGive(frameSync);

        //  技术上只需要一次：让流式传输任务知道我们至少有一帧
        //  并且如果有的话，它可以开始向客户端发送帧
        xTaskNotifyGive(tStream);

        //  立即让其他（流式传输）任务运行
        taskYIELD();

        //  如果流式传输任务已挂起自己（没有要流式传输的活动客户端）
        //  则无需从相机抓取帧。我们可以通过挂起任务来节省一些电量
        if (eTaskGetState(tStream) == eSuspended)
        {
            vTaskSuspend(NULL); // 传递NULL表示"挂起自己"
        }
    }
}

// ==== 利用可用PSRAM的内存分配器 =======================
char *allocateMemory(char *aPtr, size_t aSize)
{

    //  由于当前缓冲区太小，释放它
    if (aPtr != NULL)
        free(aPtr);

    size_t freeHeap = ESP.getFreeHeap();
    char *ptr = NULL;

    // 如果请求的内存超过当前可用堆的2/3，立即尝试PSRAM
    if (aSize > freeHeap * 2 / 3)
    {
        if (psramFound() && ESP.getFreePsram() > aSize)
        {
            ptr = (char *)ps_malloc(aSize);
        }
    }
    else
    {
        //  有足够的可用堆 - 让我们尝试分配快速RAM作为缓冲区
        ptr = (char *)malloc(aSize);

        //  如果堆上的分配失败，再给PSRAM一次机会：
        if (ptr == NULL && psramFound() && ESP.getFreePsram() > aSize)
        {
            ptr = (char *)ps_malloc(aSize);
        }
    }

    // 最后，如果内存指针为NULL，我们无法分配任何内存，这是一个终端条件。
    if (ptr == NULL)
    {
        ESP.restart();
    }
    return ptr;
}

// ==== 流式传输 ======================================================
const char HEADER[] = "HTTP/1.1 200 OK\r\n"
                      "Access-Control-Allow-Origin: *\r\n"
                      "Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321\r\n";
const char BOUNDARY[] = "\r\n--123456789000000000000987654321\r\n";
const char CTNTTYPE[] = "Content-Type: image/jpeg\r\nContent-Length: ";
const int hdrLen = strlen(HEADER);
const int bdrLen = strlen(BOUNDARY);
const int cntLen = strlen(CTNTTYPE);

// ==== 处理来自客户端的连接请求 ===============================
void handleJPGSstream(void)
{
    //  只能容纳10个客户端。该限制是WiFi连接的默认值
    if (!uxQueueSpacesAvailable(streamingClients))
        return;

    //  创建一个新的WiFi客户端对象来跟踪这个客户端
    WiFiClient *client = new WiFiClient();
    *client = server.client();

    //  立即向此客户端发送头部
    client->write(HEADER, hdrLen);
    client->write(BOUNDARY, bdrLen);

    // 将客户端推送到流式传输队列
    xQueueSend(streamingClients, (void *)&client, 0);

    // 唤醒流式传输任务（如果之前已挂起）：
    if (eTaskGetState(tCam) == eSuspended)
        vTaskResume(tCam);
    if (eTaskGetState(tStream) == eSuspended)
        vTaskResume(tStream);
}

// ==== 实际向所有已连接的客户端流式传输内容 ========================
void streamCB(void *pvParameters)
{
    char buf[16];
    TickType_t xLastWakeTime;
    TickType_t xFrequency;

    //  等待直到捕获第一帧并且有内容可发送给客户端
    ulTaskNotifyTake(pdTRUE,         /* 退出前清除通知值。 */
                     portMAX_DELAY); /* 无限阻塞。 */

    xLastWakeTime = xTaskGetTickCount();
    for (;;)
    {
        // 默认假设我们按照FPS运行
        xFrequency = pdMS_TO_TICKS(1000 / FPS);

        //  只有当有人在观看时才费心发送任何内容
        UBaseType_t activeClients = uxQueueMessagesWaiting(streamingClients);
        if (activeClients)
        {
            // 根据连接的客户端数量调整周期
            xFrequency /= activeClients;

            //  由于我们向所有人发送相同的帧，
            //  从队列前端弹出一个客户端
            WiFiClient *client;
            xQueueReceive(streamingClients, (void *)&client, 0);

            //  检查此客户端是否仍在连接
            if (!client->connected())
            {
                //  如果客户端已断开连接，删除此客户端引用
                //  并且不再将其放回队列。再见！
                delete client;
            }
            else
            {

                //  好的。这是一个活跃连接的客户端。
                //  让我们获取信号量以防止在我们
                //  服务此帧时帧发生变化
                xSemaphoreTake(frameSync, portMAX_DELAY);

                client->write(CTNTTYPE, cntLen);
                sprintf(buf, "%d\r\n\r\n", camSize);
                client->write(buf, strlen(buf));
                client->write((char *)camBuf, (size_t)camSize);
                client->write(BOUNDARY, bdrLen);

                // 由于此客户端仍在连接，将其推送到
                // 队列末尾以进行进一步处理
                xQueueSend(streamingClients, (void *)&client, 0);

                //  帧已送达。释放信号量并让其他任务运行。
                //  如果有帧切换准备好，它将在帧之间发生
                xSemaphoreGive(frameSync);
                taskYIELD();
            }
        }
        else
        {
            //  由于没有已连接的客户端，没有理由浪费电量运行
            vTaskSuspend(NULL);
        }
        //  服务每个客户端后让其他任务运行
        taskYIELD();
        vTaskDelayUntil(&xLastWakeTime, xFrequency);
    }
}

const char JHEADER[] = "HTTP/1.1 200 OK\r\n"
                       "Content-disposition: inline; filename=capture.jpg\r\n"
                       "Content-type: image/jpeg\r\n\r\n";
const int jhdLen = strlen(JHEADER);

// ==== 提供一帧JPEG图像 =============================================
void handleJPG(void)
{
    WiFiClient client = server.client();

    if (!client.connected())
        return;
    cam.run();
    client.write(JHEADER, jhdLen);
    client.write((char *)cam.getfb(), cam.getSize());
}

// ==== 处理无效的URL请求 ============================================
void handleNotFound()
{
    String message = "服务器正在运行!\n\n";
    message += "URI: ";
    message += server.uri();
    message += "\n方法: ";
    message += (server.method() == HTTP_GET) ? "GET" : "POST";
    message += "\n参数: ";
    message += server.args();
    message += "\n";
    server.send(200, "text / plain", message);
}

// ==== 初始化方法 ==================================================================
void setup()
{

    // 设置串口连接：
    Serial.begin(115200);
    delay(1000); // 等待一秒钟让串口连接

    // 配置相机
    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = Y2_GPIO_NUM;
    config.pin_d1 = Y3_GPIO_NUM;
    config.pin_d2 = Y4_GPIO_NUM;
    config.pin_d3 = Y5_GPIO_NUM;
    config.pin_d4 = Y6_GPIO_NUM;
    config.pin_d5 = Y7_GPIO_NUM;
    config.pin_d6 = Y8_GPIO_NUM;
    config.pin_d7 = Y9_GPIO_NUM;
    config.pin_xclk = XCLK_GPIO_NUM;
    config.pin_pclk = PCLK_GPIO_NUM;
    config.pin_vsync = VSYNC_GPIO_NUM;
    config.pin_href = HREF_GPIO_NUM;
    config.pin_sscb_sda = SIOD_GPIO_NUM;
    config.pin_sscb_scl = SIOC_GPIO_NUM;
    config.pin_pwdn = PWDN_GPIO_NUM;
    config.pin_reset = RESET_GPIO_NUM;
    config.xclk_freq_hz = 20000000;
    config.pixel_format = PIXFORMAT_JPEG;

    // 帧参数：选择一个
    //  config.frame_size = FRAMESIZE_UXGA;
    //  config.frame_size = FRAMESIZE_SVGA;
    //  config.frame_size = FRAMESIZE_QVGA;
    config.frame_size = FRAMESIZE_VGA;
    config.jpeg_quality = 12;
    config.fb_count = 2;

#if defined(CAMERA_MODEL_ESP_EYE)
    pinMode(13, INPUT_PULLUP);
    pinMode(14, INPUT_PULLUP);
#endif

    if (cam.init(config) != ESP_OK)
    {
        Serial.println("初始化相机错误");
        delay(10000);
        ESP.restart();
    }

    //  配置并连接到WiFi
    IPAddress ip;

    WiFi.mode(WIFI_STA);
    WiFi.begin("MIFI-7499", "limo2003"); // WIFI名称和密码
    Serial.print("连接到WiFi");
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(F("."));
    }
    ip = WiFi.localIP();
    Serial.println(F("WiFi已连接"));
    Serial.println("");
    Serial.print("流链接: http://");
    Serial.print(ip);
    Serial.println("/mjpeg/1");

    // 启动主流媒体RTOS任务
    xTaskCreatePinnedToCore(
        mjpegCB,
        "mjpeg",
        4 * 1024,
        NULL,
        2,
        &tMjpeg,
        APP_CPU);
}

void loop()
{
    vTaskDelay(1000);
}